/**
 * @file motiondetect.hpp
 *
 * @brief this is the main file impelents motion detect state machine
 *
 * @ingroup evcamera
 *
 * @author Bruce Lu
 * Contact: lzbgt@icloud.com
 *
 */

#ifndef __MOTION_DETECT_HPP__
#define __MOTION_DETECT_HPP__

#pragma onece

#include <typeinfo>
#include <sml.hpp>
#include <map>
#include <string>
#include "record_upload.h"

namespace sml = boost::sml;
extern thread_upload_args_t gUploadArgs;

namespace md{
    /// types
    struct none{};
    struct motion{};
    struct people{};
    struct mpboth{};
    /// consts
    const std::map<const std::string, int> kNumIn2Post = {{"n", 3}, {"m", 6}, {"p", 10}};
    const std::map<const std::string, int> kNumPost2None = {{"n", 2}, {"m", 4}, {"p", 8}};
    const string kKeyM= "m";
    const string kKeyN= "n";
    const string kKeyP= "p";
    const int kMaxCntMotionInPre = 5; // about 20s
    const int kMaxDuration = 60*20; // max 20 minutes duration to force generating event videos
    const int kMinMotionNum = 1;

    /// helpers
    auto event_inspection(auto e, std::map<const std::string, int> &m){
        if(std::is_same<decltype(e), people>::value){
            rlogger->info("type is people: {}",  ++m[kKeyP]);
        }else if(std::is_same<decltype(e), motion>::value){
            rlogger->info("type is motion: {}",  ++m[kKeyM]);
        }else if(std::is_same<decltype(e), mpboth>::value){
            rlogger->info("type is mpboth");
            return true;
        }else if(std::is_same<decltype(e), none>::value){
            rlogger->info("type is none: {}",  ++m[kKeyN]);
        }
        return false;
    }

    auto make_guard_fn(const std::map<const std::string, int> &konst, std::map<const std::string, int> &guards ){
        return [&konst, &guards](auto e){
            //static std::map<const std::string, int>guards = {{"m", 0}, {"p", 0}, {"n", 0}};
            bool trans = false;
            bool reset = true;
            if(event_inspection(e, guards)||(guards[kKeyM] != 0 && guards[kKeyP] != 0)){
                trans = false;
            }else if(guards[kKeyP] >= konst.at(kKeyP)){
                trans = true;
            }else if(guards[kKeyM] >= konst.at(kKeyM)){
                trans = true;
            }else if(guards[kKeyN] >= konst.at(kKeyN)){
                trans = true;
            }else{
                trans = false;
                reset = false;
            }

            if(reset){
                guards = {{"m", 0}, {"p", 0}, {"n", 0}};
            }

            return trans;
        };
    }

    struct fsm {
        auto operator()() noexcept{
            const auto guard_in2post = make_guard_fn(kNumIn2Post, guards);
            const auto guard_post2none = make_guard_fn(kNumPost2None, guards);
            const auto action_none2in = [this](auto e){
                hasMotion = 0;
                timeStart = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
                if(std::is_same<decltype(e), people>::value){
                    rlogger->info("none -p-> in, {}", timeStart/1000);
                }else if(std::is_same<decltype(e), mpboth>::value){
                    rlogger->info("none -b-> in, {}", timeStart/1000);
                    hasMotion++;
                }

                /// TODO: force event generation when max time duration reached

            };
            const auto action_pre2in = [this](auto e){
                if(timeStart==0)
                    timeStart = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
                if(std::is_same<decltype(e), people>::value){
                    rlogger->info("pre -p-> in: time {}", timeStart/1000);
                }else if(std::is_same<decltype(e), mpboth>::value){
                    rlogger->info("pre -b-> in: time {}", timeStart/1000);
                    hasMotion++;
                }
            };

            const auto action_pre2pre = [this](auto e){
                cntMotionOnly++;
                if(cntMotionOnly >= kMaxCntMotionInPre) {
                    cntMotionOnly = 0;
                    hasMotion=0;
                    timeStart = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
                }
                hasMotion++;
            };

            const auto action_post2none = [this](auto e){
                auto timeEnd = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
                if(std::is_same<decltype(e), people>::value){
                    rlogger->info("post -p-> none, {}", timeEnd/1000);
                }else if(std::is_same<decltype(e), motion>::value){
                    hasMotion++;
                    rlogger->info("post -m-> none, {}", timeEnd/1000);
                }else if(std::is_same<decltype(e), none>::value){
                    rlogger->info("post -n-> none, {}", timeEnd/1000);
                }

                if(hasMotion >= 1) {
                    upload_item_t item{timeStart, timeEnd, 7, -1, timeEnd};
                    {
                        lock_guard<mutex> lg(*gUploadArgs.mut);
                        gUploadArgs.que->push(item);
                        gUploadArgs.cv->notify_one();
                    }
                }else{
                    rlogger->warn("ignore event since motion {} < {}", hasMotion, kMinMotionNum);
                }
                
                hasMotion = 0;
                timeStart = 0;
                
            };
            
            const auto may_generate_event = [this]{
                auto now = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
                if(timeStart != 0 && ((now  - timeStart) >= kMaxDuration *1000 /*|| this->is("none"_s)*/)){
                    upload_item_t item{timeStart, now, 7, -1, now};
                    {
                        lock_guard<mutex> lg(*gUploadArgs.mut);
                        gUploadArgs.que->push(item);
                        gUploadArgs.cv->notify_one();
                    }
                    timeStart = now;
                    hasMotion=0;
                    return true;
                }

                return false;
            };

            // const auto generate_event = [this]{
            //     auto now = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
            //     if(timeStart != 0){
            //         upload_item_t item{timeStart, now, 7, -1, now};
            //         {
            //             lock_guard<mutex> lg(*gUploadArgs.mut);
            //             gUploadArgs.que->push(item);
            //             gUploadArgs.cv->notify_one();
            //         }
            //         rlogger->info("event generated: tss {}, tse {}, type {}", timeStart, now, 7);
            //     }else{
            //         //rlogger->error("failed generate event: tss {}, tse {}, type {}", timeStart, now, 7);
            //     }
            //     timeStart = 0;
            //     hasMotion = 0;
            // };
            
            using namespace sml;
            return make_transition_table(
                *"init"_s = "none"_s,
                "none"_s + event<none>/[]{rlogger->info("none -> none");} = "none"_s,
                "none"_s + event<motion>/[]{rlogger->info("none -m-> pre");} = "pre"_s,
                "none"_s + event<people>/action_none2in = "in"_s,
                "none"_s + event<mpboth>/action_none2in = "in"_s,

                // pre
                "pre"_s + event<none>/[]{rlogger->info("pre -n-> none");} = "none"_s,
                "pre"_s + event<motion>/action_pre2pre = "pre"_s,
                "pre"_s + event<people>/action_pre2in = "in"_s,
                "pre"_s + event<mpboth>/action_pre2in = "in"_s,

                // in
                "in"_s + event<none> [guard_in2post]/[]{rlogger->info("in -n-> post");} = "post"_s,
                "in"_s + event<motion> [guard_in2post]/[]{rlogger->info("in -m-> post");} = "post"_s,
                "in"_s + event<people> [guard_in2post]/[]{rlogger->info("in -p-> post");} = "post"_s,
                "in"_s + event<mpboth>/guard_in2post,
                "in"_s + sml::on_entry<_> / may_generate_event,

                // post
                "post"_s + event<mpboth>/guard_post2none = "in"_s,
                "post"_s + event<people> [guard_post2none]/action_post2none = "none"_s,
                "post"_s + event<motion> [guard_post2none]/action_post2none = "none"_s,
                "post"_s + event<none> [guard_post2none]/action_post2none = "none"_s
                // "post"_s + sml::on_entry<_> / may_generate_event,
                // "none"_s + sml::on_entry<_> / generate_event
            );
        }
        private:
            int hasMotion{0};
            int cntMotionOnly = 0;
            int64_t timeStart{0};
            std::map<const std::string, int>guards{{"m", 0}, {"p", 0}, {"n", 0}};
    };
}

#endif