////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2022 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#pragma once

#include "vec3.h"
#include "mat3.h"
#include "ringbuffer.h"
#include "quaternion.h"
#include "NcLibWarpInternal.h"

/***********************/

#if (defined(_WIN32) || defined(_WIN64)) && !defined(__EIS_CALIBRATION_BLUR_MASKING__) && !defined(ANDROID) && !defined( __KLOCWORK__ )
#define _EIS_INTERNAL_DEBUG_
#endif

#if defined(_EIS_INTERNAL_DEBUG_)

#ifndef __UNIT_TEST_WRAPPER_FOLDER_LOCATION__
#define __UNIT_TEST_WRAPPER_FOLDER_LOCATION__
#if (defined(__EIS_UNIT_TEST_WRAPPER__) || defined(__EIS_MEM_ALLOC_TEST__))
extern char g_output_csv_base_folder[];
#else
static const char g_output_csv_base_folder[] = ".";
#endif /* __EIS_UNIT_TEST_WRAPPER__ */
#endif /* __UNIT_TEST_WRAPPER_FOLDER_LOCATION__ */
#endif /* _EIS_INTERNAL_DEBUG_ */

typedef void(rotate_sample)(sample_data_t&);  //TODO (etsairi): replace with: typedef sample_data_t&(rotate_sample)(const sample_data_t&)

class is_valid_stat
{
public:
    uint32_t    _num_errors;
    uint32_t    _num_warnings;
    uint32_t    _num_info;

    is_valid_stat()
    :
        _num_errors(0),
        _num_warnings(0),
        _num_info(0)
    {}

    inline is_valid_stat& operator+(const is_valid_stat& in)
    {
        _num_errors     += in._num_errors;
        _num_warnings   += in._num_warnings;
        _num_info       += in._num_info;

        return *this;
    }
};

typedef Vec3<double>        Vec, Ang;
typedef Mat3<double>        Mat, Hom;
typedef Quaternion<double>  Quat;

/** DIS output class */
class DisStabilizeOutput
{
public:
    Hom         dis_out2in;
    uint32_t    frame_idx;
    bool        has_output;

public:
    DisStabilizeOutput() : frame_idx(0) , has_output(false) {};
    ~DisStabilizeOutput() {};
};

/** single sample in quaternion representation */
typedef struct sample_quat_s
{
    Quat        quat;
    uint64_t    ts;      /**< Time stamp in microseconds */
} sample_quat_t;

#ifndef _EIS_USE_QUATERNION_STAB_
    typedef Ang                                         Orientation;
    typedef sample_data_t                               sample_orientation_t;
    #define EIS_EULER_TO_ORIENTATION(_ang)              (_ang)
    #define EIS_ORIENTATION_TO_EULER(_ori)              (_ori)
#else
    typedef Quat                                        Orientation;
    typedef sample_quat_t                               sample_orientation_t;
    #define EIS_EULER_TO_ORIENTATION(_ang)              (Quat::from_euler((_ang)))
    #define EIS_ORIENTATION_TO_EULER(_ori)              ((_ori).to_euler())
#endif /* _EIS_USE_QUATERNION_STAB_ */

class Sat  //TODO (etsairi): use inheritance or is it less readable?
{
    public:
        Hom     _mat;
        bool    _is_unity;

        Sat(const Hom* sat_in2out)
        :
            _mat((NULL == sat_in2out) ? Hom().eye() : *sat_in2out),
            _is_unity((NULL == sat_in2out) ? true : sat_in2out->is_unity())
        {}

        Sat(const Sat* sat_in2out)
        :
            _mat(((NULL == sat_in2out) || (true == sat_in2out->_is_unity)) ? Hom().eye() : sat_in2out->_mat),
            _is_unity((NULL == sat_in2out) ? true : sat_in2out->_is_unity)
        {}
};

class Dis
{
public:
    Hom     _mat;
    bool    _is_valid;

    Dis(const Hom& mat, bool is_valid)
    :
        _mat(mat),
        _is_valid(is_valid)
    {}
};

//TODO (etsairi): move from here to a new file:
// -------------------------------------------------------------------------------------------------------------------
//#include <initializer_list>
#include <cmath>
#include "point2d.h"

class BoundingBox_t
{
public:
    BoundingBox_t() {}

    BoundingBox_t(double x_max, double x_min, double y_max, double y_min) :
        x_max(x_max),
        x_min(x_min),
        y_max(y_max),
        y_min(y_min)
    {}

    double x_max, x_min, y_max, y_min;
};

template <std::size_t N>
class InterpolationWeights {
public:
    InterpolationWeights() {}
//    InterpolationWeights(std::initializer_list<double> vals) {
//        IS_ASSERT(vals.size() == N);
//        uint32_t i = 0;
//        std::initializer_list<double>::iterator it;
//        for (it = vals.begin(); it != vals.end(); ++it)
//            w[i++] = *it;
//    }
    // accesses weight i
    double &operator[](uint32_t i) { return w[i]; };
    // accesses weight i
    const double &operator[](uint32_t i) const { return w[i]; };

protected:
    // vertices
    double w[N];
};

// for some reason Polygon (withouut _t) causes errors, need to debug
template <std::size_t N>
class Polygon_t
{
public:
    Polygon_t() {}

//    Polygon_t(std::initializer_list<Point2d> vals)
//    {
//        IS_ASSERT(vals.size() == N);
//        uint32_t i = 0;
//        std::initializer_list<Point2d>::iterator it;
//        for (it = vals.begin(); it != vals.end(); ++it)
//        {
//            v[i++] = *it;
//        }
//    }

    virtual ~Polygon_t() {};

    // checks if point inside a polygon
    virtual inline bool point_inside(const Point2d &point) const = 0;

    // calculates area
    virtual double area() const = 0;

    // calculates bounding box
    virtual BoundingBox_t bounding_box() const = 0;

    // accesses vertex i
    Point2d &operator[](uint32_t i) { return v[i]; };

    // accesses vertex i
    const Point2d &operator[](uint32_t i) const { return v[i]; };

protected:
    // vertices
    Point2d v[N];

    static double signed_distance(const Point2d & p1, const Point2d & p2, const Point2d & p3)
    {
        return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
    }
};

class Quadrangle : public Polygon_t<4>
{
public:
    class QuadrangleWeights : public InterpolationWeights<2>
    {
        public:
            QuadrangleWeights() {}
            //QuadrangleWeights(std::initializer_list<double> vals) : InterpolationWeights(vals) {}
            QuadrangleWeights(double t, double s) { w[0] = t; w[1] = s;}
    };

    Quadrangle() {};

    //Quadrangle(std::initializer_list<Point2d> vals) : Polygon_t(vals) {}
    Quadrangle(Point2d p1, Point2d p2, Point2d p3, Point2d p4) { v[0] = p1; v[1] = p2; v[2] = p3; v[3] = p4; }

    ~Quadrangle() {};

    /* Returns true if the point in located on the segment otherwise returns false */
    static bool isPointInSegment(const NcLibWarpGridCoord* seg[2], const NcLibWarpGridCoord* point)
    {
        const NcLibWarpGridCoord minSegment = { /* x */ NCLIB_MIN(seg[0]->x , seg[1]->x),
            /* y */ NCLIB_MIN(seg[0]->y , seg[1]->y) };

        const NcLibWarpGridCoord maxSegment = { /* x */ NCLIB_MAX(seg[0]->x , seg[1]->x),
            /* y */ NCLIB_MAX(seg[0]->y , seg[1]->y) };

        return (((point->x >= minSegment.x) && (point->x <= maxSegment.x) &&
            (point->y >= minSegment.y) && (point->y <= maxSegment.y)));
    }

    /* Returns 0 in case points are collinear negative number in case of clockwise orientation and
    * positive number for counterclockwise orientation
    */
    static int32_t calcOrientation(
        const NcLibWarpGridCoord* p1,
        const NcLibWarpGridCoord* p2,
        const NcLibWarpGridCoord* p3)
    {
        const double det = ((double)p2->y - (double)p1->y)*((double)p3->x - (double)p2->x) -
            ((double)p2->x - (double)p1->x)*((double)p3->y - (double)p2->y);

        if (det <= (-NC_LIB_WARP_INTERNAL_EPS))
        {
            return -1; /* clockwise */
        }
        else if (det >= NC_LIB_WARP_INTERNAL_EPS)
        {
            return  1; /* counterclockwise */
        }
        else
        {
            return 0;  /* collinear */
        }
    }

    /** Calculate the orientation between line and a point */
    static int32_t calcOrientationLinePoint(
        const NcLibWarpGridCoord* seg[2],
        const NcLibWarpGridCoord* point)
    {
        return calcOrientation(seg[0], seg[1], point);
    }

    // inside or on the border
    virtual inline bool point_inside(const Point2d &point) const
    {
        float gridMaxX = -INFINITY;

        static const uint32_t num_points = 4;

        NcLibWarpGridCoord polygonExternal[num_points];
        for (uint32_t ii = 0; ii < num_points; ii++)
        {
            polygonExternal[ii].x = (float)v[ii].x;
            polygonExternal[ii].y = (float)v[ii].y;
            gridMaxX = MAX(gridMaxX, polygonExternal[ii].x);
        }

        const bool isPointOnOrOutside = NcLibInternalIsPointCertainlyOnOrOutsideBoundaryOfPolygon(
            (float)(point.x),
            (float)(point.y),
            polygonExternal,
            num_points,
            gridMaxX);

        if (false == isPointOnOrOutside) // inside
        {
            return true;
        }
        else // maybe on border
        {
            const NcLibWarpGridCoord pointExternal = { (float)(point.x), (float)(point.y) };
            for (uint32_t edge_idx = 0; edge_idx < num_points; edge_idx++)
            {
                const NcLibWarpGridCoord* edge[2];

                if (edge_idx < num_points - 1)
                {
                    edge[0] = &(polygonExternal[edge_idx]);
                    edge[1] = &(polygonExternal[edge_idx + 1]);
                }
                else // last edge closes the polygon
                {
                    edge[0] = &(polygonExternal[num_points - 1]);
                    edge[1] = &(polygonExternal[0]);
                }
                const bool is_collinear = (0 == calcOrientation(edge[0], edge[1], &pointExternal));
                if (true == is_collinear)
                {
                    const bool is_point_in_polygon_edge = isPointInSegment(edge, &pointExternal);
                    if (true == is_point_in_polygon_edge)
                    {
                        return true; // on border
                    }
                }
            }
            return false; // not on border
        }
    }

    double barycentric_weight_t(const Point2d & P) const
    {
        // weights are inversely proportional to distance from edges
        const double length_top = (v[0] - v[1]).norm();
        const double length_bot = (v[2] - v[3]).norm();
        const double area_top = abs(signed_distance(v[0], v[1], P));
        const double area_bot = abs(signed_distance(v[2], v[3], P));

        // above is optimization, equivalent to:
        // w_i = area_i/length_i
        // t = w_2/(w_1+w_2)
        const double t1 = area_top*length_bot;
        const double t2 = area_bot*length_top;
        double t = t1 / (t1 + t2);
        return t;
    }

    inline Point2d interp(const QuadrangleWeights & weights) const
    {
        const double t = weights[0];
        const double s = weights[1];
        const Point2d res_top = v[0] * s + v[1] * (1 - s);
        const Point2d res_bot = v[3] * s + v[2] * (1 - s);
        return res_top*t + res_bot*(1 - t);
    }

    virtual double area() const
    {
        return (v[0].x - v[2].x)*(v[1].y - v[3].y) + (v[1].x - v[3].x)*(v[2].y - v[0].y);
    }

    //virtual BoundingBox_t bounding_box() override {
    //  BoundingBox_t res;
    //  res.x_min = MIN(v[0].x, v[1].x);
    //  res.x_max = MAX(v[2].x, v[3].x);
    //  res.y_min = MIN(v[0].y, v[3].y);
    //  res.y_max = MAX(v[1].y, v[2].y);
    //  return res;
    //}

    virtual BoundingBox_t bounding_box() const
    {
        BoundingBox_t res;
        res.x_min = MIN4(v[0].x, v[1].x, v[2].x, v[3].x);
        res.x_max = MAX4(v[0].x, v[1].x, v[2].x, v[3].x);
        res.y_min = MIN4(v[0].y, v[1].y, v[2].y, v[3].y);
        res.y_max = MAX4(v[0].y, v[1].y, v[2].y, v[3].y);
        return res;
    }
};

//TODO (etsairi): move to another file
class SceneDetection
{
#if defined(_EIS_INTERNAL_DEBUG_)
    class scene_detection_debug_data
    {
    public:
#define FILE_NAME_scene_detection_update                IS_DBG_UTILS_DEFAULT_DIR"dump_scene_detection__update.csv"
#define FILE_NAME_scene_detection_process_panning       IS_DBG_UTILS_DEFAULT_DIR"dump_scene_detection__process_panning.csv"

    public:
        FILE* _update;
        FILE* _process_panning;

    public:
        scene_detection_debug_data(int32_t& status)
        :
            _update(NULL),
            _process_panning(NULL)
        {
            IS_ASSERT(IS_RET_SUCCESS == status);
            errno_t err;

#if !defined(_WIN32) && !defined(_WIN64)
            IS_ERR("Compiling with internal EIS debug struct enabled");
#endif /* !_WIN32 && !_WIN64 */

            char output_csv[1024];

            {
                (void)snprintf(output_csv, ARR_SIZE(output_csv) - 1, "%s/%s", g_output_csv_base_folder, FILE_NAME_scene_detection_update);

                err = IS_FOPEN(&_update, output_csv, "w");
                if (err != 0)
                {
                    IS_ERR(IS_ERR_GENERAL_ERROR, "Cannot open file %s", output_csv);
                    status = IS_RET_GENERAL_ERROR;
                }
            }


            {
                (void)snprintf(output_csv, ARR_SIZE(output_csv) - 1, "%s/%s", g_output_csv_base_folder, FILE_NAME_scene_detection_process_panning);

                err = IS_FOPEN(&_process_panning, output_csv, "w");
                if (err != 0)
                {
                    IS_ERR(IS_ERR_GENERAL_ERROR, "Cannot open file %s", output_csv);
                    status = IS_RET_GENERAL_ERROR;
                }
            }
        }

        ~scene_detection_debug_data()
        {
            IS_FCLOSE(_update);
            IS_FCLOSE(_process_panning);
        }
    };
#endif /* _EIS_INTERNAL_DEBUG_ */

public:
    enum SceneTypes
    {
        PANNING_0   = (1 << 30),    /**< Panning is done in 1st axis direction */
        PANNING_1   = (1 << 29),    /**< Panning is done in 2nd axis direction */
        PANNING_2   = (1 << 28),    /**< Panning is done in 3rd axis direction */

        TRIPOD      = 0,            /**< Device is mounted on tripod, without user is touching the device at all */
        HANDHELD    = 1,            /**< Device is hand held, while user is trying to be as steady is possible */
        WALKING     = 2,            /**< Device is held while the person is walking in normal phase */
        RUNNING     = 3,            /**< Device is held while the person is running (light running) */

        PANNING         = 4,            /**< Panning is done in at least on one direction */
        STRONG_PANNING  = 5,            /**< Strong panning is done at least on one direction  */

        OTHER           = 6,            /**< All other use-cases */

        NUM_OF_SCENE_TYPES,         /**< Invalid value, must be the last member */
        INVALID = NUM_OF_SCENE_TYPES,

        SCENE_TYPES_MASK = 0x0000FFFF,
        PANNING_MASK     = 0xF0000000
    };

private:
    static const uint32_t _num_gyro_channels = 3;    /**< Number of gyro channels */

    //TODO (etsairi): note that below parameters are correlated, cannot change one without changing the other
    static const uint32_t _gyro_data_buffer_num = 2 * 5;    /**< Number of backtracking frame data */
    static const uint32_t _np_change_th         = 2;    /**< The direction change point (positive->negative or negative->positive) number threshold for judge as continuous panning */

    RingBuffer<sample_data_t> _gyro_data_md; /**< Store the gyro data for scene detection */

private:
    double          _scene_gyro_th[SceneTypes::NUM_OF_SCENE_TYPES];             /**< Units: [rad/sec] */
    uint32_t        _scene_frames_counter[SceneTypes::NUM_OF_SCENE_TYPES];      /**< Current counter for scene type */
    uint32_t        _scene_frames_counter_th[SceneTypes::NUM_OF_SCENE_TYPES];   /**< Number of frames before update the scene type */
    const float     _above_noise_floor_ratio;                                   /**< minimal ratio of samples needed before updating the scene type */

    uint64_t        _last_sample_ts;
    SceneTypes      _scene_type;
    SceneTypes      _last_panning_result;


    //TODO (etsairi): go over the documentation, could be clearer
    //TODO (etsairi): do we need different threshold per axis (_panning_angle_th and _np_change_th)? what about roll ? did we profile it?s
    double          _panning_angle_th;          /**< The angle threshold for judge as panning */
    double          _strong_panning_angle_th;   /**< The angle threshold for judge as panning */

    const double          _walk_mean_th                 = 0.1;
    const double          _strong_panning_mean_th       = 0.4;

    const double          _abs_delta_handheld_th        = 0.15;
    const double          _abs_delta_strong_panning_th  = 0.6;

    static const uint32_t       _max_panning_history = 12;
    static const uint32_t       _max_walking_history = 30;
    static const uint32_t       _max_running_history = 30;

    int32_t         _walking_history;
    int32_t         _running_history;
    int32_t         _panning_history;

#if defined(_EIS_INTERNAL_DEBUG_)
    scene_detection_debug_data*   _debug;
#endif /* _EIS_INTERNAL_DEBUG_ */

private:
    inline SceneTypes gyro_data_based_scene_detection() const
    {
        IS_ASSERT(_gyro_data_buffer_num == _gyro_data_md.size());   // Otherwise should not be called
        IS_ASSERT(true == _gyro_data_md.full());                    // Otherwise should not be called

        // Start with "Other" scene type as it is the less limiting one... if we start with tripod
        // or hand-held it might affect DIS or EISv3 solution
        SceneTypes result = SceneTypes::INVALID;

        const uint32_t gyro_data_size = MAX(1, _gyro_data_md.size()); // Sanity check

        // Represent scene types for each direction
        // Used for debug
        SceneTypes result_by_abs_delta[_num_gyro_channels];
        SceneTypes result_by_abs_gyro_delta[_num_gyro_channels];
        SceneTypes result_by_mean_abs[_num_gyro_channels];
        SceneTypes result_by_mean[_num_gyro_channels];
        SceneTypes result_by_turning[_num_gyro_channels];
        SceneTypes result_voted[_num_gyro_channels];

        // holds the score for each channel
        int32_t   result_cnt[_num_gyro_channels];

        // The sum/average value of the absolute gyro data in each direction
        double sum_abs_data[_num_gyro_channels];
        double sum_data[_num_gyro_channels];
        double mean_abs_data[_num_gyro_channels];
        double mean_data[_num_gyro_channels];

        // The direction change point number
        uint32_t np_point_sum[_num_gyro_channels];

        // The max/min gyro data in each direction
        double gyro_max[_num_gyro_channels];
        double gyro_min[_num_gyro_channels];
        double gyro_abs_max[_num_gyro_channels];

        for (uint32_t idx = 0; idx < _num_gyro_channels; idx++)
        {
            result_by_abs_delta[idx]        = SceneTypes::HANDHELD;
            result_by_abs_gyro_delta[idx]   = SceneTypes::HANDHELD;
            result_by_mean_abs[idx]         = SceneTypes::HANDHELD;
            result_by_mean[idx]             = SceneTypes::HANDHELD;
            result_by_turning[idx]          = SceneTypes::HANDHELD;
            result_voted[idx]               = SceneTypes::HANDHELD;

            result_cnt[idx]         = 0;

            sum_abs_data[idx]   = 0;
            sum_data[idx]       = 0;
            mean_abs_data[idx]  = 0;
            mean_data[idx]      = 0;
            np_point_sum[idx]   = 0;
            gyro_max[idx]       = 0;
            gyro_min[idx]       = 0;
            gyro_abs_max[idx]   = 0;
        }

        for (uint32_t idx = 1; idx < gyro_data_size; idx++)
        {
            const int32_t index = gyro_data_size - idx; //TODO (etsairi): why walking backwards ? can walk forward? you also have at_history() API

            // Sanity checks
            IS_ASSERT(index >= 0);
            IS_ASSERT(index < (int32_t)gyro_data_size);

            for (uint32_t ch_idx = 0; ch_idx < _num_gyro_channels; ch_idx++)
            {
                // Calculate sum value
                sum_abs_data[ch_idx] += ABS(_gyro_data_md[index].data[ch_idx]);
                sum_data[ch_idx] += _gyro_data_md[index].data[ch_idx];

                // if gyro(n)*gyro(n - 1) < 0, we called gyro(i) is turning points.
                if (_gyro_data_md[index].data[ch_idx] * _gyro_data_md[index - 1].data[ch_idx] < 0)
                {
                    np_point_sum[ch_idx]++;
                }

                // Find max value
                if (_gyro_data_md[index].data[ch_idx] > gyro_max[ch_idx])
                {
                    gyro_max[ch_idx] = _gyro_data_md[index].data[ch_idx];
                }

                // Find min value
                if (_gyro_data_md[index].data[ch_idx] < gyro_min[ch_idx])
                {
                    gyro_min[ch_idx] = _gyro_data_md[index].data[ch_idx];
                }

                if (ABS(_gyro_data_md[index].data[ch_idx]) > gyro_abs_max[ch_idx])
                {
                    gyro_abs_max[ch_idx] = ABS(_gyro_data_md[index].data[ch_idx]);
                }
            }
        }

        // check according to mixed channels
        for (uint32_t base_idx = 0; base_idx < _num_gyro_channels; base_idx++)
        {
            for (uint32_t idx = 0; idx < _num_gyro_channels; idx++)
            {
                if (idx == base_idx)
                {
                    continue;
                }

                const uint32_t next_idx = (idx + 1) % _num_gyro_channels;

                if ((next_idx == idx) || (next_idx == base_idx))
                {
                    continue;
                }

                // result_by_abs_gyro_delta
                {
                    const double max_delta_bsae_vs_idx = (gyro_abs_max[base_idx] - gyro_abs_max[idx]);
                    const double max_delta_base_vs_next_idx = (gyro_abs_max[base_idx] - gyro_abs_max[next_idx]);

                    if ((max_delta_bsae_vs_idx > (2 * _abs_delta_strong_panning_th)) &&
                        (max_delta_base_vs_next_idx > (2 * _abs_delta_strong_panning_th)))
                    {
                        result_by_abs_gyro_delta[base_idx] = STRONG_PANNING;
                        result_cnt[base_idx]    += 4;
                        result_cnt[idx]         -= 4;
                        result_cnt[next_idx]    -= 4;
                    }
                    else if ((max_delta_bsae_vs_idx > _abs_delta_strong_panning_th) &&
                        (max_delta_base_vs_next_idx > _abs_delta_strong_panning_th))
                    {
                        result_by_abs_gyro_delta[base_idx] = STRONG_PANNING;
                        result_cnt[base_idx]    += 2;
                        result_cnt[idx]         -= 2;
                        result_cnt[next_idx]    -= 2;
                    }
                    else if ((max_delta_bsae_vs_idx > (_abs_delta_strong_panning_th / 2)) &&
                        (max_delta_base_vs_next_idx > (_abs_delta_strong_panning_th / 2)))
                    {
                        result_by_abs_gyro_delta[base_idx] = PANNING;
                        result_cnt[base_idx]    += 1;
                        result_cnt[idx]         -= 1;
                        result_cnt[next_idx]    -= 1;
                    }
                }

                // result_by_abs_delta
                {
                    const double abs_delta = ABS(gyro_max[idx] - gyro_min[idx]);

                    if (abs_delta > _abs_delta_handheld_th)
                    {
                        result_by_abs_delta[idx] = OTHER;
                        result_cnt[idx]         += 1;
                    }
                    else
                    {
                        result_by_abs_delta[idx] = HANDHELD;
                        result_cnt[idx]         -= 1;
                        result_cnt[next_idx]    -= 1;
                    }
                }
            }
        }

        // Calculate mean value of each direction
        for (uint32_t idx = 0; idx < _num_gyro_channels; idx++)
        {
            mean_abs_data[idx]  = sum_abs_data[idx] / gyro_data_size;
            mean_data[idx]      = sum_data[idx] / gyro_data_size;

            // For horizon direction    // yaw (1st axis)
            // For vertical direction   // pitch (2nd axis)
            // For rotation direction   // roll (3rd axis)

            // result_by_mean_abs
            {
                if (mean_abs_data[idx] < _walk_mean_th) // below walk threshold  - 0.1
                {
                    result_by_mean_abs[idx] = WALKING;
                }
                else if (mean_abs_data[idx] < _strong_panning_mean_th / 2) // below panning threshold  x in [0.1 , 0.2)
                {
                    result_by_mean_abs[idx] = PANNING;
                    result_cnt[idx] += 1;
                }
                else if (mean_abs_data[idx] < _strong_panning_mean_th) // below panning threshold  x in [0.2 , 0.4)
                {
                    result_by_mean_abs[idx] = STRONG_PANNING;
                    result_cnt[idx] += 1;
                }
                else if (mean_abs_data[idx] < (2 * _strong_panning_mean_th)) // x  in [0.4, 0.8)
                {
                    result_by_mean_abs[idx] = STRONG_PANNING;
                    result_cnt[idx] += 2;
                }
                else if (mean_abs_data[idx] > (2 * _strong_panning_mean_th)) // x > 0.4
                {
                    result_by_mean_abs[idx] = STRONG_PANNING;
                    result_cnt[idx] += 4;
                }
            }

            // result_by_mean
            {
                if (ABS(mean_data[idx]) < _walk_mean_th) // below walk threshold  - 0.1
                {
                    result_by_mean[idx] = WALKING;
                }
                else if (ABS(mean_data[idx]) < _strong_panning_mean_th) // below panning threshold  x in [0.1 , 0.4)
                {
                    result_by_mean[idx] = PANNING;
                    result_cnt[idx] += 1;

                }
                else if (ABS(mean_data[idx]) > _strong_panning_mean_th) // x > 0.4
                {
                    result_by_mean[idx] = STRONG_PANNING;
                    result_cnt[idx] += 2;
                }
            }

            // result_by_turning
            {
                switch (np_point_sum[idx])
                {
                    case 0:
                    {
                        result_cnt[idx]         += 5;
                        result_by_turning[idx]  = STRONG_PANNING;
                        break;
                    }
                    case 1:
                    {
                        result_cnt[idx]         += 3;
                        result_by_turning[idx]  = PANNING;
                        break;
                    }
                    case 2:
                    {
                        result_cnt[idx]         -= 1;
                        result_by_turning[idx]  = WALKING;
                        break;
                    }
                    case 3:
                    {
                        result_cnt[idx]         -= 3;
                        result_by_turning[idx]  = WALKING;
                        break;
                    }
                    case 4:
                    {
                        result_cnt[idx]         -= 4;
                        result_by_turning[idx]  = WALKING;
                        break;
                    }
                    default:
                    {
                        IS_ASSERT(np_point_sum[idx] > 4);
                        result_cnt[idx]         -= 5;
                        result_by_turning[idx]  = WALKING;
                        break;
                    }
                }
            }
        }

        // check voting
        uint32_t mask = 0;
        uint32_t cnt_walking = 0;
        uint32_t cnt_panning = 0;
        uint32_t cnt_strong_panning = 0;
        uint32_t cnt_handheld = 0;

        for (uint32_t  idx = 0; idx < _num_gyro_channels; idx++)
        {
            if (result_cnt[idx] < 3)
            {
                if (result_by_abs_delta[idx] == HANDHELD)
                {
                    result_voted[idx] = HANDHELD;
                    cnt_handheld++;
                }
                else
                {
                    result_voted[idx] = WALKING;
                    cnt_walking++;
                }
            }
            else if (result_cnt[idx] <= 5)
            {
                result_voted[idx] = WALKING;
                cnt_walking++;
            }
            else if (result_cnt[idx] <= 7)
            {
                result_voted[idx] = PANNING;
                result = PANNING;
                mask |= (PANNING_2 << idx);
                cnt_panning++;
            }
            else
            {
                if ((STRONG_PANNING == result_by_mean_abs[idx]) ||
                    (STRONG_PANNING == result_by_mean[idx]))
                {
                    result_voted[idx] = STRONG_PANNING;
                    result = STRONG_PANNING;
                    cnt_strong_panning++;
                }
                else
                {
                    result_voted[idx] = PANNING;
                    result = PANNING;
                }

                mask |= (PANNING_2 << idx);
                cnt_panning++;
            }
        }

        if (3 == cnt_panning)
        {
            if (3 == cnt_strong_panning)
            {
                result = WALKING; // should be Running ...
            }
            else if (1 == cnt_strong_panning)
            {
                // This is a special case, when there is a really strong panning on one axis and it affects the panning decision on other axis as well
                result = (SceneTypes)((uint32_t)STRONG_PANNING | mask);
            }
            else
            {
                result = WALKING;
            }
        }
        else if ((true == is_panning(result_voted[0])) || (true == is_panning(result_voted[1])) || (true == is_panning(result_voted[2])))
        {
            result = (SceneTypes)((uint32_t)result | mask);
        }
        else
        {
            if (3 == cnt_handheld)
            {
                result = HANDHELD;
            }
            else if (cnt_walking > 2)
            {
                result = WALKING;
            }
            else
            {
                result = HANDHELD;
            }
        }


#if 0

        // below heuristics is not used
        // decision will go as follow :
        // Handheld + tripod is like before
        //
        // Panning will be when on the following occurs:
        //  1. panning in one axis + hand held on other axis
        //  2. strong panning on one axis and walking on hand held on other axis
        //  3. in case of "diagonal" panning (for example moving to top-right) we expect strong panning in pitch and yaw - roll should be hand-held
        //
        // Walking will be one the following occurs:
        //  1. when more then two channels are passing the panning threshold
        //  2. _walking_history is non-zero
        //
        // Running:will be one the following occurs:
        //  1. When more then two channels are passing the strong panning threshold
        //  2. _running_history is non-zero


        const uint32_t result_sum = result_by_pan[0] + result_by_pan[1] + result_by_pan[2];
        if ((4 < result_by_pan[0]) || (4 < result_by_pan[1]) || (4 < result_by_pan[2]))
        {
            result = ((result_by_pan[0] > 4) || (result_by_pan[1] > 4) || (result_by_pan[2] > 4)) ? STRONG_PANNING : PANNING;

            if (4 <= result_by_pan[0])
            {
                result = (SceneTypes)((uint32_t)result | (uint32_t)PANNING_0);
            }

            if (4 <= result_by_pan[1])
            {
                result = (SceneTypes)((uint32_t)result | (uint32_t)PANNING_1);
            }

            if (4 <= result_by_pan[2])
            {
                result = (SceneTypes)((uint32_t)result | (uint32_t)PANNING_2);
            }
        }
        else
        {
            if (result_sum <= 3)
            {
                result = SceneTypes::HANDHELD;
            }
            else if (result_sum > 3 && result_sum <= 6)
            {
                result = SceneTypes::WALKING;
            }
            else if (result_sum > 6)
            {
                result = SceneTypes::RUNNING;
            }
        }
#endif /* 0 */

#if defined(_EIS_INTERNAL_DEBUG_)
        {
            IS_ASSERT(NULL != _debug);
            IS_ASSERT(NULL != _debug->_process_panning);

            scene_detection_t X = to_scene_detection_t(_scene_type);

            fprintf(_debug->_process_panning,
                "%u, %u, %u, "
                "%.15lf, %.15lf, %.15lf, "
                "%.15lf, %.15lf, %.15lf, "
                "%u, %u, %u, "
                "%.15lf, %.15lf, %.15lf, "
                "%.15lf, %.15lf, %.15lf, "
                "%d, %d, %d, "
                "%u"
                "\n",
                (uint32_t)result_by_abs_delta[0], (uint32_t)result_by_abs_delta[1], (uint32_t)result_by_abs_delta[2],
                mean_data[0], mean_data[1], mean_data[2],
                mean_abs_data[0], mean_abs_data[1], mean_abs_data[2],
                np_point_sum[0], np_point_sum[1], np_point_sum[2],
                gyro_max[0], gyro_max[1], gyro_max[2],
                gyro_min[0], gyro_min[1], gyro_min[2],
                result_cnt[0], result_cnt[1], result_cnt[2],
                (uint32_t)((uint32_t)result & (uint32_t)SCENE_TYPES_MASK)
            );
        }
#endif /* _EIS_INTERNAL_DEBUG_ */

        return result;
    }

public:
    SceneDetection(
        const is_init_data_sensor*  init_sensors,
        int32_t&                    status)
    :
        _gyro_data_md(_gyro_data_buffer_num),
        _above_noise_floor_ratio(0.25),
        _last_sample_ts(0),
        _scene_type(SceneTypes::INVALID),
        _last_panning_result(SceneTypes::OTHER),
        _walking_history(0),
        _running_history(0),
        _panning_history(0)
    {
        IS_ASSERT(IS_RET_SUCCESS == status);
        IS_ASSERT(NULL != init_sensors);

        if (0 == _gyro_data_md.maxSize())
        {
            IS_ERR(IS_ERR_OUT_OF_MEMORY, "Out of Memory");
            status = IS_RET_OUT_OF_MEMORY;
        }

        for (uint32_t idx = 0; idx < ARR_SIZE(_scene_gyro_th); idx++)
        {
            _scene_gyro_th[idx] = 0;
        }

        memset(_scene_frames_counter, 0, sizeof(_scene_frames_counter));
        memset(_scene_frames_counter_th, 0, sizeof(_scene_frames_counter_th));

        // Tripod: part of calibration procedure, not tunable by user
        _scene_gyro_th[SceneTypes::TRIPOD] = init_sensors->is_chromatix_info.general.gyro_noise_floor;
        _scene_frames_counter_th[SceneTypes::TRIPOD] = 25;

        // Different types motion angle threshold (Here use absolute+average value as threshold)
        _scene_gyro_th[SceneTypes::WALKING] = 0.045;
        _scene_gyro_th[SceneTypes::RUNNING] = 0.350;


        if (0 == init_sensors->is_chromatix_info.general__motion_detection_parameter_mode)
        {
            // Hand-held
            _scene_gyro_th[SceneTypes::HANDHELD]                = 0.135;
            _scene_frames_counter_th[SceneTypes::HANDHELD]      = 25;

           // The angle threshold for judge as panning (Here us absolute angle as threshold)
            _panning_angle_th                                   = (0.35 / 2);
            _strong_panning_angle_th                            = 0.35;
        }
        else
        {
            _scene_gyro_th[SceneTypes::HANDHELD]                = init_sensors->is_chromatix_info.general__motion_detection_handheld_th;
            _scene_frames_counter_th[SceneTypes::HANDHELD]      = 25;

            // The angle threshold for judge as panning (Here us absolute angle as threshold)
            _panning_angle_th                                   = init_sensors->is_chromatix_info.general__motion_detection_panning_th;
            _strong_panning_angle_th                            = init_sensors->is_chromatix_info.general__motion_detection_strong_panning_th;
        }


#if defined(_EIS_INTERNAL_DEBUG_)
        _debug = new(std::nothrow) scene_detection_debug_data(status);
        if (NULL == _debug)
        {
            IS_ERR(IS_ERR_OUT_OF_MEMORY, "failed to allocate scene_detection_debug_data");
            status = IS_RET_OUT_OF_MEMORY;
        }
#endif /*_EIS_INTERNAL_DEBUG_  */
    };

    ~SceneDetection()
    {
#if defined(_EIS_INTERNAL_DEBUG_)
        _DELETE(_debug);
#endif /*_EIS_INTERNAL_DEBUG_  */
    };

    inline void set_first_frame()
    {
        _gyro_data_md.clear();
        _scene_type             = SceneTypes::INVALID;
        _last_panning_result    = SceneTypes::OTHER;
        _last_sample_ts         = 0;
        _walking_history        = 0;
        _running_history        = 0;
        _panning_history        = 0;
    };

    int32_t update(
        const is_input_distortion_correction_t* eis3_input,
        uint32_t                                num_dropped_frames, /**< Number for dropped frames w.r.t. previews frame. In case of many frame drops need to have different treatment */
        bool                                    was_sensor_changed)
    {
        IS_ASSERT(NULL != eis3_input);

        if (0 == eis3_input->gyro_data.num_elements)
        {
            return IS_RET_SUCCESS; // Nothing to do
        }

        IS_ASSERT(NULL != eis3_input->gyro_data.samples);

        uint32_t num_elements_above_noise_floor = 0;
        uint32_t num_elements_above_handheld_th = 0;

        bool was_first                  = false;
        uint32_t first_new_sample_idx   = 0;

        for (uint32_t idx = 0; idx < eis3_input->gyro_data.num_elements; idx++)
        {
            // Find first relevant sample (removes samples from previous frames)
            if (_last_sample_ts >= eis3_input->gyro_data.samples[idx].ts)
            {
                continue;   /* remove samples from the past */
            }

            if (false == was_first)
            {
                was_first = true;
                first_new_sample_idx = idx;
            }

            // Estimate gyro bias when phone is not moved at all
            {
                if ((ABS(eis3_input->gyro_data.samples[idx].data[0]) >= _scene_gyro_th[SceneTypes::TRIPOD]) ||
                    (ABS(eis3_input->gyro_data.samples[idx].data[1]) >= _scene_gyro_th[SceneTypes::TRIPOD]) ||
                    (ABS(eis3_input->gyro_data.samples[idx].data[2]) >= _scene_gyro_th[SceneTypes::TRIPOD]))
                {
                    num_elements_above_noise_floor++;

                    if ((ABS(eis3_input->gyro_data.samples[idx].data[0]) >= _scene_gyro_th[SceneTypes::HANDHELD]) ||
                        (ABS(eis3_input->gyro_data.samples[idx].data[1]) >= _scene_gyro_th[SceneTypes::HANDHELD]) ||
                        (ABS(eis3_input->gyro_data.samples[idx].data[2]) >= _scene_gyro_th[SceneTypes::HANDHELD]))
                    {
                        num_elements_above_handheld_th++;
                    }
                }
            }

            _last_sample_ts = eis3_input->gyro_data.samples[idx].ts;
        }

        // if we ignore old samples it also means that we need to consider only the number of new samples when calculating the ratio
        const uint32_t num_new_samples                      = eis3_input->gyro_data.num_elements - first_new_sample_idx;
        const uint32_t num_new_samples_per_missing_frame    = num_new_samples / (num_dropped_frames + 1);

        if (num_new_samples_per_missing_frame <= 2)
        {
            if (true == was_sensor_changed)
            {
                // Nothing to do here... got samples from previous time, continue with existing decision
                IS_HIGH(IS_ERR_GENERAL_WARNING, "Sensor switch was detected and not enough gyro samples were supplied to detect scene. using previous decision");
                goto Exit;
            }
            else //TODO (etsairi): we need to add a check on input for this ... no reason to even get here ...
            {
                IS_ASSERT(0); //TODO (etsairi): is this even possible?
                IS_ERR(IS_ERR_INVALID_INPUT, "Gyro data drop is heavy for scene detection!");

                _scene_type = SceneTypes::INVALID;
                goto Exit;
            }
        }

        // Scene detection
        {
            if (num_elements_above_noise_floor <= (_above_noise_floor_ratio * num_new_samples))
            {
                _scene_frames_counter[SceneTypes::TRIPOD]++;
                _scene_frames_counter[SceneTypes::HANDHELD] = 0;

                if ((SceneTypes::INVALID == _scene_type) ||
                    (_scene_frames_counter[SceneTypes::TRIPOD] >= _scene_frames_counter_th[SceneTypes::TRIPOD]))
                {
                    _scene_type = SceneTypes::TRIPOD;
                }
            }
            else if (num_elements_above_handheld_th <= (_above_noise_floor_ratio * num_new_samples))
            {
                _scene_frames_counter[SceneTypes::TRIPOD] = 0;
                _scene_frames_counter[SceneTypes::HANDHELD]++;

                if ((SceneTypes::INVALID == _scene_type) ||
                    (_scene_frames_counter[SceneTypes::HANDHELD] >= _scene_frames_counter_th[SceneTypes::HANDHELD]))
                {
                    _scene_type = SceneTypes::HANDHELD;
                }
            }
            else
            {
                //TODO (etsairi): also need some _scene_frames_counter_th here?
                _scene_frames_counter[SceneTypes::TRIPOD] = 0;
                _scene_frames_counter[SceneTypes::HANDHELD] = 0;
                _scene_type = SceneTypes::OTHER;
            }
        }

        // Update internal RingBuffer to hold first new sample of each frame.
        // In case of frame drop compensate on missing frames by taking the more samples according to the number of frame drops detected
        {
            //TODO (etsairi): take the last sample from each frame and not the first, this way you gain more "future" data

            IS_ASSERT(first_new_sample_idx < eis3_input->gyro_data.num_elements); // Sanity check
            first_new_sample_idx = MIN(first_new_sample_idx, eis3_input->gyro_data.num_elements - 1); // make sure not to have bugs in case there is no new data...

            // make up for frame drops, usually when we have frame drop we get gyro data for the dropped frame
            {
                // need to push data for current frame and for all dropped frames
                for (uint32_t idx = 0; idx < (num_dropped_frames + 1); idx++)
                {
                    //TODO (etsairi): make this assert more thigh
                    IS_ASSERT((first_new_sample_idx + num_new_samples_per_missing_frame * idx) < eis3_input->gyro_data.num_elements); // Sanity check

                    _gyro_data_md.push(eis3_input->gyro_data.samples[first_new_sample_idx + num_new_samples_per_missing_frame * idx]); // buffer to new frame data (approximated to fit the new frame)
                }
            }
        }

        if ((true == _gyro_data_md.full()) &&                                               //if buffer is not full, there is nothing to do
            ((SceneTypes::HANDHELD != _scene_type) && (SceneTypes::TRIPOD != _scene_type))) // not already defined to be tripod or hand-held...
        {
            //TODO (etsairi): currently this function can only tell if we have panning
            const SceneTypes tmp_scene_type = gyro_data_based_scene_detection();

            if (true == is_panning(tmp_scene_type))
            {
                _panning_history = MIN((int32_t)_max_panning_history, _panning_history + 1);
                _last_panning_result = tmp_scene_type;
            }
            else
            {
                _panning_history = MAX(0, _panning_history - 1);
            }

            if (tmp_scene_type == SceneTypes::WALKING)
            {
                _walking_history = MIN((int32_t)_max_walking_history, _walking_history + 1);
            }
            else
            {
                if (tmp_scene_type == SceneTypes::HANDHELD)
                {
                    _walking_history = MAX(0, _walking_history - 4);
                }
                else
                {
                    _walking_history = MAX(0, _walking_history - 1);
                }
            }

            if (tmp_scene_type == SceneTypes::RUNNING)
            {
                _running_history = MIN((int32_t)_max_running_history, _running_history + 1);
            }
            else
            {
                _running_history = MAX(0, _running_history - 1);
            }

            // Update _scene_type
            {
                _scene_type = (HANDHELD == tmp_scene_type) ? HANDHELD : OTHER;

                if ((_walking_history > 0) || (_panning_history > 0))
                {
                    if (_walking_history > _panning_history)
                    {
                        _scene_type = SceneTypes::WALKING;
                    }
                    else if ((_panning_history >= _walking_history) ||
                             (_panning_history >= (_max_panning_history * 3.0 / 4)))
                    {
                        if (true == is_panning(tmp_scene_type))
                        {
                            _scene_type = tmp_scene_type;
                        }
                        else
                        {
                            IS_ASSERT(true == is_panning(_last_panning_result));
                            _scene_type = _last_panning_result;
                        }
                    }
                }
            }

#if 0
            if ((0 == _walking_history) && (_panning_history > 0))
            {
                _scene_type = tmp_scene_type;
            }
            else if ((_walking_history > 0) && (0 == _panning_history))
            {
                _scene_type = SceneTypes::WALKING;
            }
            else if ((_walking_history > 0) && (_panning_history > 0))
            {
                _scene_type = SceneTypes::WALKING;
            }
            else
            {
                _scene_type = OTHER;
            }
#endif /* 0 */
        }
        else
        {
            _panning_history = MAX(0, _panning_history - 1);
            _running_history = MAX(0, _running_history - 1);
            _walking_history = MAX(0, _walking_history - 4);

#if defined(_EIS_INTERNAL_DEBUG_)
            {
                IS_ASSERT(NULL != _debug);
                IS_ASSERT(NULL != _debug->_process_panning);

                scene_detection_t X = to_scene_detection_t(_scene_type);

                fprintf(_debug->_process_panning,
                    "%u, %u, %u, "
                    "%.15lf, %.15lf, %.15lf, "
                    "%.15lf, %.15lf, %.15lf, "
                    "%u, %u, %u, "
                    "%.15lf, %.15lf, %.15lf, "
                    "%.15lf, %.15lf, %.15lf, "
                    "%d, %d, %d, "
                    "%u"
                    "\n",
                    0u, 0u,0u ,
                    0.0f, 0.0f, 0.0f,
                    0.0f, 0.0f, 0.0f,
                    0u, 0u, 0u,
                    0.0f, 0.0f, 0.0f,
                    0.0f, 0.0f, 0.0f,
                    0, 0, 0,
                    0
                    );
            }
#endif /* _EIS_INTERNAL_DEBUG_ */
        }

    Exit:

#if defined(_EIS_INTERNAL_DEBUG_)
        {
            IS_ASSERT(NULL != _debug);
            IS_ASSERT(NULL != _debug->_update);

            scene_detection_t X = to_scene_detection_t(_scene_type);

            fprintf(_debug->_update,
                "%u, %u, %u, %u, %u, %u, %u, %u,"
                "%" PRIu64 ","
                "%.15lf, %.15lf, %.15lf,"
                "%d, %d, %d"
                "\n",
                eis3_input->frame_id,
                eis3_input->active_sensor_idx,
                (uint32_t)X.scene_type,
                (uint32_t)X.is_panning[0],
                (uint32_t)X.is_panning[1],
                (uint32_t)X.is_panning[2],
                num_dropped_frames,
                was_sensor_changed,
                eis3_input->gyro_data.samples[first_new_sample_idx].ts,
                eis3_input->gyro_data.samples[first_new_sample_idx].data[0],
                eis3_input->gyro_data.samples[first_new_sample_idx].data[1],
                eis3_input->gyro_data.samples[first_new_sample_idx].data[2],
                _panning_history, _walking_history, _running_history
            );
        }
#endif /* _EIS_INTERNAL_DEBUG_ */

        return IS_RET_SUCCESS;
    }


    static inline bool is_panning(SceneTypes in)
    {
        const SceneTypes tmp = (SceneTypes)((uint32_t)in & (uint32_t)SceneTypes::SCENE_TYPES_MASK);
        return (SceneTypes::PANNING == tmp) || (SceneTypes::STRONG_PANNING == tmp);
    }

    static inline bool is_strong_panning(SceneTypes in)
    {
        const SceneTypes tmp = (SceneTypes)((uint32_t)in & (uint32_t)SceneTypes::SCENE_TYPES_MASK);
        return (SceneTypes::STRONG_PANNING == tmp);
    }

    inline SceneTypes get_scene_type() const
    {
        return _scene_type;
    }

    inline bool has_output() const
    {
        if (true == _gyro_data_md.full())
        {
            return true;
        }
        else // beginning of the video, not enough frames to estimate panning \ walking cases
        {
            if (OTHER != _scene_type)
            {
                //Sanity check: according to gyro values we can estimate if tripod or hand held case
                IS_ASSERT((TRIPOD == _scene_type)|| (HANDHELD == _scene_type));
                return true;
            }
        }

        return false;
    }

    static inline scene_detection_t to_scene_detection_t(SceneTypes in)
    {
        // To save some switch code later
        {
            // make sure that the conversion is correct
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_TRIPOD    == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::TRIPOD));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_HANDHELD  == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::HANDHELD));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_WALKING   == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::WALKING));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_RUNNING   == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::RUNNING));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_PANNING   == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::PANNING));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_STRONG_PANNING   == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::STRONG_PANNING));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_OTHER     == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::OTHER));
            IS_COMPILE_TIME_ASSERT((uint32_t)IS_SCENE_INVALID   == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::INVALID));

            // make sure that we didn't miss anything
            IS_COMPILE_TIME_ASSERT((uint32_t)(((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::OTHER) + 1) == ((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)SceneTypes::NUM_OF_SCENE_TYPES));
        }

        scene_detection_t out;

        out.scene_type = (is_scene_types_e)((uint32_t)SceneTypes::SCENE_TYPES_MASK & (uint32_t)in);
        if (true == is_panning(in))
        {
            IS_ASSERT((IS_SCENE_PANNING == out.scene_type) || (IS_SCENE_STRONG_PANNING == out.scene_type));

            out.is_panning[0] = (0 != ((uint32_t)SceneTypes::PANNING_0 & (uint32_t)in));
            out.is_panning[1] = (0 != ((uint32_t)SceneTypes::PANNING_1 & (uint32_t)in));
            out.is_panning[2] = (0 != ((uint32_t)SceneTypes::PANNING_2 & (uint32_t)in));
        }
        else
        {
            IS_ASSERT((IS_SCENE_PANNING != out.scene_type) && (IS_SCENE_STRONG_PANNING != out.scene_type));

            out.is_panning[0] = 0;
            out.is_panning[1] = 0;
            out.is_panning[2] = 0;
        }

        return out;
    }
};
