#include "OutputLibrary.h"
#include <tracking/Tracker.h>
#include <cmath>
#include <misc/EventAnalysis.h>
#include <file/CSVExport.h>
#include <misc/cnpy_wrapper.h>

namespace Output {
    using namespace gui;
    
    LibraryCache::Ptr _default_cache = std::make_shared<LibraryCache>();
    std::map<std::string, Library::FunctionType> _cache_func;
    std::map<std::string, std::vector<std::pair<OptionsList<Modifiers>, Calculation>>> _options_map;
    Output::Library::default_options_type _output_defaults;
    std::mutex _output_variables_lock;

    LibraryCache::Ptr LibraryCache::default_cache() {
        return _default_cache;
    }
    
    std::vector<std::string> Library::functions() {
        std::vector<std::string> ret;
        
        for (auto &p : _cache_func) {
            ret.push_back(p.first);
        }
        
        return ret;
    }
    
    void LibraryCache::clear() {
        std::lock_guard<std::recursive_mutex> lock(_cache_mutex);
        _cache.clear();
    }
    
    void Library::clear_cache() {
        //std::lock_guard<std::recursive_mutex> lock(_cache_mutex);
        //_cache.clear();
        _default_cache->clear();
    }
    
    void Library::Init() {
        // add the standard functions
        _cache_func.clear();
        _default_cache->clear();
        
        const float cm_per_px = FAST_SETTINGS(cm_per_pixel);
        static const float CENTER_X = SETTING(output_centered) ? SETTING(video_size).value<Size2>().width * 0.5f * cm_per_px : 0;
        static const float CENTER_Y = SETTING(output_centered) ?SETTING(video_size).value<Size2>().height * 0.5f * cm_per_px : 0;
        
        GlobalSettings::map().register_callback((void*)&_options_map, [](const sprite::Map&, const std::string& name, const sprite::PropertyType&)
        {
            if (name == "output_graphs" || name == "output_default_options" || name == "midline_resolution")
            {
                auto graphs = SETTING(output_graphs).value<std::vector<std::pair<std::string, std::vector<std::string>>>>();
                std::lock_guard<std::mutex> lock(_output_variables_lock);
                _output_defaults = SETTING(output_default_options).value<Output::Library::default_options_type>();
                _options_map.clear();
                
                //Debug("Recalculating options map...");
                
                std::vector<std::string> remove;
                for (auto &c : _cache_func) {
                    if(utils::beginsWith(c.first, "bone")) {
                        remove.push_back(c.first);
                    }
                }
                for (auto &r : remove) {
                    _cache_func.erase(r);
                }
                
                for (uint32_t i=1; i<FAST_SETTINGS(midline_resolution); i++) {
                    Library::add("bone"+std::to_string(i), [i]
                     _LIBFNC({
                         // angular speed at current point
                         auto midline = fish->midline(frame);
                         
                         if (midline) {
                             float prev_angle = 0.0;
                             
                             if(i > 1) {
                                 auto line = midline->segments().at(i-1).pos - midline->segments().at(i-2).pos;
                                 float abs_angle = atan2(line.y, line.x);
                                 
                                 prev_angle = abs_angle;
                             }
                             
                             auto line = midline->segments().at(i).pos - midline->segments().at(i-1).pos;
                             float abs_angle = atan2(line.y, line.x) - prev_angle;
                             
                             return abs_angle;
                         } else
                             Warning("No midline.");
                         
                         return infinity<float>();
                    }));
                    
                    //SETTING(output_default_options).value<std::map<std::string, std::vector<std::string>>>()["bone"+std::to_string(i)] = { "*2" };
                }

                for (auto &instance : graphs) {
                    auto &fname = instance.first;
                    auto &options = instance.second;
                    
                    if(_cache_func.count(fname) == 0) {
                        Warning("There is no function called '%S'.", &fname);
                        continue;
                    }
                    
                    OptionsList<Modifiers> modifiers;
                    Calculation func;
                    
                    if(_output_defaults.count(fname)) {
                        auto &o = _output_defaults.at(fname);
                        for(auto &e : o) {
                            if(!parse_modifiers(e, modifiers))
                                // function is something other than identity
                                func = parse_calculation(e);
                        }
                    }
                    
                    for(auto &e : options) {
                        if(!parse_modifiers(e, modifiers))
                            // function is something other than identity
                            func = parse_calculation(e);
                    }
                    
                    _options_map[fname].push_back({ modifiers, func });
                }
            }
        });
        
        //SETTING(output_graphs) = std::vector<std::string>{
            /*Functions::X,
            Functions::Y,
            Functions::VX,
            Functions::VY,
            "SPEED_OLD",
            Functions::SPEED_SMOOTH,
            Functions::SPEED,
            Functions::ACCELERATION_SMOOTH,
            Functions::ACCELERATION,
            Functions::MIDLINE_OFFSET,
            Functions::ORIENTATION,
            Functions::NEIGHBOR_DISTANCE,
            Functions::BORDER_DISTANCE,*/
            //"NEIGHBOR_VECTOR_T"
            //"RELATIVE_ANGLE",
            //"DOT_V"
            /*Functions::BORDER_DISTANCE,
            Functions::NEIGHBOR_DISTANCE,
            "DOT_V",
            "L_V"*/
            /*"X", "Y", "VX", "VY",*/ /*"BORDER_DISTANCE",*/ /*"ORIENTATION",*/ ///*"ACCELERATION_SMOOTH", "NEIGHBOR_DISTANCE",*/ /*"SPEED_OLD",*/ "SPEED"//, "MIDLINE_OFFSET"
         //   ,"X", "ACCELERATION", "ANGULAR_V", "ANGULAR_A"
        //};
        
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
        
        using namespace track;
        
        _cache_func[Functions::X.name()] =
            LIBFNC( return (props->pos(Units::CM_AND_SECONDS, smooth).x - CENTER_X); );
        
        _cache_func[Functions::Y.name()] =
            LIBFNC( return (props->pos(Units::CM_AND_SECONDS, smooth).y - CENTER_Y); );
        
        _cache_func[Functions::VX.name()] = LIBFNC ( return props->v(Units::CM_AND_SECONDS, smooth).x; );
        _cache_func[Functions::VY.name()] = LIBFNC( return props->v(Units::CM_AND_SECONDS, smooth).y; );
        _cache_func["AX"] = LIBFNC ( return props->a(Units::CM_AND_SECONDS, smooth).x; );
        _cache_func["AY"] = LIBFNC( return props->a(Units::CM_AND_SECONDS, smooth).y; );
        
        _cache_func[Functions::ANGLE.name()] = LIBFNC( return props->angle(smooth); );
        _cache_func[Functions::ANGULAR_V.name()] = LIBFNC( return props->angular_velocity(Units::DEFAULT, smooth); );
        _cache_func[Functions::ANGULAR_A.name()] = LIBFNC( return props->angular_acceleration(Units::DEFAULT, smooth); );
        
        _cache_func[Functions::SPEED.name()] = LIBFNC( return props->speed(Units::CM_AND_SECONDS, smooth); );
        _cache_func[Functions::ACCELERATION.name()] = LIBFNC( return props->acceleration(Units::CM_AND_SECONDS, smooth); );
        
        _cache_func[Functions::MIDLINE_OFFSET.name()] = LIBFNC({
            Vec2 spt(0, 0);
            size_t samples = 0;
            
            const bool normalize = SETTING(output_normalize_midline_data);
            
            for(long_t f = frame-long_t(smooth?FAST_SETTINGS(smooth_window):0); f<=frame+long_t(smooth?FAST_SETTINGS(smooth_window):0); f++)
            {
                auto midline = normalize ? fish->fixed_midline(frame) : fish->midline(frame);
                if (!midline)
                    return infinity<float>();
                
                auto &pts = midline->segments();
                
                // tail offset from zero
                auto complete = pts.back().pos - pts.front().pos;
                spt += complete;
                samples++;
            }
            spt/=float(samples);
            float angle = cmn::atan2(spt.y, spt.x);
            
            return angle;
        });
        
        _cache_func["variance"] = LIBFNC({
            //var = mean(abs(x - x.mean())**2)
            Vec2 mean(0);
            float samples = 0;
            std::vector<float> all;
            
            const long_t offset = 100;//FAST_SETTINGS(frame_rate)*0.5;
            for(long_t i=frame - offset; i<=frame + offset; i++)
            {
                auto midline = fish->midline(i);
                if(midline) {
                    auto &pts = midline->segments();
                    
                    // tail offset from zero
                    auto complete = pts.back().pos - pts.front().pos;
                    all.push_back(cmn::atan2(complete.y, complete.x));
                    mean += complete;
                    samples++;
                }
            }
            
            if(samples == 0 || !fish->midline(frame))
                return infinity<float>();
            
            mean = mean / samples;
            float mean_angle = cmn::atan2(mean.y, mean.x);
            
            auto midline = fish->midline(frame);
            auto &pts = midline->segments();
            
            // tail offset from zero
            auto complete = pts.back().pos - pts.front().pos;
            float a = cmn::atan2(complete.y, complete.x);
            float var = SQR(cmn::abs(a - mean_angle));
            /*float var = 0;
            for(auto& a: all) {
                auto v = abs(a - mean_angle);
                var += SQR(v);
            }
            var /= all.size();*/
            
            return var;
        });
        
        _cache_func["normalized_midline"] = LIBFNC({
            float value = EventAnalysis::midline_offset(info.fish, frame);
            if(cmn::isinf(value))
                return infinity<float>();
            
            long_t samples = 1;
            
            for(long_t f = frame-long_t(smooth?FAST_SETTINGS(smooth_window):0); f<=frame+long_t(smooth?FAST_SETTINGS(smooth_window):0); f++)
            {
                if(f != frame) {
                    float sample = EventAnalysis::midline_offset(info.fish, f);
                    if(!cmn::isinf(sample)) {
                        value += sample;
                        samples++;
                    }
                }
            }
            
            return value / samples;
        });
        
        _cache_func[Functions::MIDLINE_DERIV.name()] = LIBFNC({
            float current = get("normalized_midline", info, frame);
            float previous = get("normalized_midline", info, frame-1);
            
            if(cmn::isinf(previous))
                previous = 0;
            if(cmn::isinf(current))
                return infinity<float>();
            
            return current - previous;
        });
        
        _cache_func[Functions::BINARY.name()] = LIBFNC({
            if(frame >= fish->start_frame()+1 && frame <= fish->end_frame()-1)
            {
                //Vec2 p0(x-1, cache_access(fish, Cache::MIDLINE, x-1));
                Vec2 p1(frame, get(Functions::MIDLINE_OFFSET.name(), info, frame));
                Vec2 p2(frame+1, get(Functions::MIDLINE_OFFSET.name(), info, frame+1));
                
                int c = crosses_abs_height(p1, p2, SETTING(limit).value<float>());
                return c == 0 ? infinity<float>() : c;
            }
            
            return infinity<float>();
        });
        
        _cache_func[Functions::BORDER_DISTANCE.name()] = LIBFNC({
            if(SETTING(video_mask)) {
                // circular tank
                Vec2 center(CENTER_X, CENTER_Y);
                const float radius = min(center.x, center.y);
                
                auto pos = props->pos(Units::CM_AND_SECONDS);
                float d = cmn::abs(sqrt(SQR(pos.x - center.x) + SQR(pos.y - center.y)) - radius);
                
                return d;
                
            } else {
                // rectangular tank
                Size2 size = SETTING(video_size);
                cv::Rect2f r(0, 0, size.width * FAST_SETTINGS(cm_per_pixel), size.height * FAST_SETTINGS(cm_per_pixel));
                
                auto pt = props->pos(Units::CM_AND_SECONDS);
                float d0 = min(cmn::abs(r.x - pt.x), cmn::abs(r.y - pt.y));
                float d1 = min(cmn::abs(r.width - pt.x), cmn::abs(r.height - pt.y));
                
                return min(d0, d1);
            }
            
            return infinity<float>();
        });
        
        _cache_func[Functions::NEIGHBOR_DISTANCE.name()] = LIBFNC({
            const auto pos = props->pos(Units::CM_AND_SECONDS);
            const auto individuals = Tracker::active_individuals(frame);
            
            float d = 0.0;
            float samples = 0;
            
            const PhysicalProperties *oc;
            for (auto other: individuals) {
                if (other != fish && (oc = other->centroid(frame))) {
                    auto opos = oc->pos(Units::CM_AND_SECONDS);
                    d += euclidean_distance(pos, opos);
                    samples++;
                }
            }
            
            return samples ? d / samples : infinity<float>();
        });
        
        _cache_func["time"] = LIBGLFNC({
            (void)info;
            auto props = Tracker::properties(frame);
            if(!props)
                return infinity<float>();
            return props->time;
        });
        
        _cache_func["timestamp"] = LIBGLFNC({
            (void)info;
            
            auto props = Tracker::properties(frame);
            if(!props)
                return infinity<float>();
            return props->org_timestamp;
        });
        
        _cache_func["frame"] = LIBGLFNC({
            (void)info;
            
            auto props = Tracker::properties(frame);
            if(!props)
                return infinity<float>();
            return frame;
        });
        
        _cache_func["missing"] = LIBGLFNC({
            if(!info.fish || !info.fish->has(frame))
                return 1;
            return 0;
        });
        
        //_cache_func["time"] = LIBFNC( return props->time(); );
        
        _cache_func["NEIGHBOR_VECTOR_T"] = LIBFNC({
           auto head = fish->head(frame);
                                                  
           if(head) {
               const auto a = fish->centroid_posture(frame)->pos();
               const auto individuals = Tracker::active_individuals(frame);
               
               auto angle = -head->angle();
               
               Vec2 ad(cos(angle), -sin(angle));
               ad /= length(ad);
               
               for (auto other: individuals) {
                   auto oh = other->centroid_posture(frame);
                   if (other != fish && oh)
                   {
                       float oangle = -oh->angle();
                       if (cmn::abs(angle_difference(oangle, angle)) > M_PI * 0.25) {
                           continue;
                       }
                       
                       oangle += M_PI * 0.5;
                       
                       auto v = oh->pos();
                       if(length(v - a) > 100) {
                           continue;
                       }
                       
                       Vec2 vd(cos(oangle), -sin(oangle));
                       vd /= length(vd);
                       
                       auto at = cross((a - v), vd) / cross(vd, ad);
                       Vec2 s = a + ad * at;
                       
                       at = (std::signbit(at) ? (-1) : 1) * length(v - a);
                       
                       return at;
                   }
               }
           }
           
           return infinity<float>();
        });
        
        _cache_func["RELATIVE_ANGLE"] = LIBFNC({
           const auto a0 = get(Functions::ANGLE.name(), info, frame);
           const auto individuals = Tracker::active_individuals(frame);
           const auto h0 = props->pos();
           
           const PhysicalProperties *oc;
           for (auto other: individuals) {
               if (other != fish && (oc = other->centroid(frame))) {
                   info.fish = other;
                   
                   auto a1 = get(Functions::ANGLE.name(), info, frame);
                   auto h1 = oc->pos();
                   
                   Vec2 line;
                   if(other->identity().ID() > fish->identity().ID())
                       line = h1 - h0;
                   else
                       line = h0 - h1;
                   
                   //auto angle = atan2(line.y, line.x);
                   
                   Vec2 dir0(cos(a0), -sin(a0));
                   Vec2 dir1(cos(a1), -sin(a1));
                   
                   line = line / length(line);
                   dir0 /= length(dir0);
                   dir1 /= length(dir1);
                   
                   auto angle0 = cmn::abs(dot(line, dir0));
                   auto angle1 = cmn::abs(dot(line, dir1));
                   
                   return angle1 - angle0;
               }
           }
           
           return infinity<float>();
        });
        
        _cache_func["L_V"] = LIBFNC({
            const Vec2 v(get(Functions::VX.name(), info, frame), get(Functions::VY.name(), info, frame));
            const auto individuals = Tracker::instance()->active_individuals(frame);

            float d = 0.0;
            float samples = 0;

            for (auto other: individuals) {
               if (other != fish && other->centroid(frame)) {
                   info.fish = other;
                   
                   const Vec2 ov(get(Functions::VX.name(), info, frame), get(Functions::VY.name(), info, frame));
                   d += euclidean_distance(v, ov);
                   samples++;
               }
            }

            return d / samples;
        });
        
        _cache_func["DOT_V"] = LIBFNC({
            Vec2 v(get(Functions::VX.name(), info, frame), get(Functions::VY.name(), info, frame));
            const auto individuals = Tracker::instance()->active_individuals(frame);

            for (auto other: individuals) {
              if (other != fish && other->centroid(frame)) {
                  info.fish = other;
                  
                  Vec2 ov(get(Functions::VX.name(), info, frame), get(Functions::VY.name(), info, frame));
                  float n = length(v) * length(ov);
                  
                  if(!length(v) || !length(ov))
                      return infinity<float>();
                  
                  return cmn::abs(atan2(v.y, v.x) - atan2(ov.y,  ov.x));
              }
            }

            Warning("NO OTHER FISH");
            return infinity<float>();
        });
        
        _cache_func["tailbeat_threshold"] = LIBFNC( return SETTING(limit).value<float>(); );
        _cache_func["tailbeat_peak"] = LIBFNC( return SETTING(event_min_peak_offset).value<float>(); );
        
        _cache_func["threshold_reached"] = LIBFNC({
            return EventAnalysis::threshold_reached(info.fish, frame) ? M_PI * 0.3 : infinity<float>();
        });
        
        _cache_func["sqrt_a"] = LIBFNC({
            return EventAnalysis::midline_offset(info.fish, frame);
        });
        
        _cache_func["outline_size"] = LIBFNC({
            auto o = fish->outline(frame);
            if(o)
                return o->size();
            else
                return infinity<float>();
        });
        
        _cache_func["outline_std"] = LIBFNC({
            std::vector<float> all;
            float average = 0;
            
            for (long_t i=frame-5; i<=frame+5; i++) {
                auto o = fish->outline(i);
                if(o) {
                    all.push_back(o->size());
                    average += all.back();
                }
            }
            
            if(all.empty())
                return infinity<float>();
            if(all.size() == 1)
                return 1;
            
            float sum = 0;
            average /= float(all.size());
            average = fish->outline_size();
            
            for (auto v : all)
                sum += SQR(v - average);
            sum /= float(all.size()-1);
            
            //Debug("%f", sqrt(sum) / (average * 0.05));
            
            return sqrt(sum) / (average * 0.5);
        });
        
        _cache_func["events"] = LIBFNC({
            auto events = EventAnalysis::events();
            auto it = events->map().find(info.fish);
            if(it != events->map().end()) {
                for(auto &e : it->second.events) {
                    if(e.second.begin <= frame && e.second.end >= frame) {
                        delete events;
                        return M_PI * 0.25;
                    }
                }
            }
            
            delete events;
            return 0;
        });
        
        _cache_func["event_energy"] = LIBFNC({
            auto events = EventAnalysis::events();
            auto it = events->map().find(info.fish);
            if(it != events->map().end()) {
                for(auto &e : it->second.events) {
                    if(e.second.begin <= frame && e.second.end >= frame) {
                        float energy = e.second.energy;
                        delete events;
                        return energy;
                    }
                }
            }
            
            delete events;
            return 0;
        });
        
        _cache_func["event_acceleration"] = LIBFNC({
            auto events = EventAnalysis::events();
            auto it = events->map().find(info.fish);
            if(it != events->map().end()) {
                for(auto &e : it->second.events) {
                    if(e.second.begin <= frame && e.second.end >= frame) {
                        float angle = e.second.acceleration;//length(e.second.acceleration);
                        ///angle = atan2(e.second.acceleration.y, e.second.acceleration.x);
                        delete events;
                        return angle;
                    }
                }
            }
            
            delete events;
            return 0;
        });
        
        _cache_func["event_direction_change"] = LIBFNC({
            auto events = EventAnalysis::events();
            auto it = events->map().find(info.fish);
            if(it != events->map().end()) {
                for(auto &e : it->second.events) {
                    if(e.second.begin <= frame && e.second.end >= frame) {
                        float angle = e.second.direction_change;//length(e.second.acceleration);
                        ///angle = atan2(e.second.acceleration.y, e.second.acceleration.x);
                        delete events;
                        return angle;
                    }
                }
            }
            
            delete events;
            return 0;
        });
        
        _cache_func["v_direction"] = LIBFNC({
            auto events = EventAnalysis::events();
            auto it = events->map().find(info.fish);
            if(it != events->map().end()) {
                for(auto &e : it->second.events) {
                    if(e.second.begin <= frame && e.second.end >= frame) {
                        Vec2 before(0, 0);
                        Vec2 after(0, 0);
                        float samples = 0;
                        
                        for(long_t f=e.second.begin - 50; f<=e.second.begin; f+=2) {
                            auto p = fish->centroid_posture(f);
                            if(p) {
                                before += p->v();
                                samples++;
                            }
                        }
                        
                        before /= samples;
                        samples = 0;
                        
                        for(long_t f=e.second.end; f<=e.second.end+50; f+=2) {
                            auto p = fish->centroid_posture(f);
                            if(p) {
                                after += p->v();
                                samples++;
                            }
                        }
                        
                        after /= samples;
                        
                        delete events;
                        
                        auto d_angle = atan2(after.y, after.x)-atan2(before.y, before.x);
                        return atan2(sin(d_angle), cos(d_angle));
                    }
                }
            }
            
            delete events;
            return 0;
            //return atan2(props->a(Units::CM_AND_SECONDS).y, props->a(Units::CM_AND_SECONDS).x);
        });
        
        _cache_func["segment_length"] = LIBFNC({
            auto midline = fish->midline(frame);
            
            if (midline) {
                return length(midline->segments().at(1).pos - midline->segments().at(0).pos) * FAST_SETTINGS(cm_per_pixel);
            }
            
            return infinity<float>();
        });
        
        _cache_func["consecutive"] = LIBFNC({
            auto segment = fish->segment_for(frame);
            
            if (segment) {
                return segment->length();
            }
            
            return infinity<float>();
        });
        
        _cache_func["blobid"] = LIBFNC({
            auto blob = fish->compressed_blob(frame);
            
            if (blob) {
                return blob->blob_id();
            }
            
            return infinity<float>();
        });
        
        _cache_func["num_pixels"] = LIBFNC({
            auto blob = fish->blob(frame);
            
            if (blob) {
                return blob->num_pixels();
            }
            
            return infinity<float>();
        });
        
        _cache_func["pixels_squared"] = LIBFNC({
            auto blob = fish->blob(frame);
            
            if (blob) {
                return blob->bounds().width * blob->bounds().height;
            }
            
            return infinity<float>();
        });
        
        _cache_func["midline_x"] = LIBFNC({
            auto midline = fish->midline(frame);
            
            if (midline) {
                auto blob = fish->blob(frame);
                return (blob->bounds().pos().x + midline->offset().x) * FAST_SETTINGS(cm_per_pixel);
            }
            
            return infinity<float>();
        });
        
        _cache_func["midline_y"] = LIBFNC({
            auto midline = fish->midline(frame);
            
            if (midline) {
                auto blob = fish->blob(frame);
                return (blob->bounds().pos().y + midline->offset().y) * FAST_SETTINGS(cm_per_pixel);
            }
            
            return infinity<float>();
        });
        
        _cache_func["global"] = LIBGLFNC({
            Vec2 average(0);
            float samples = 0;
            
            for(auto fish : Tracker::instance()->active_individuals(frame)) {
                if(fish->has(frame)) {
                    PhysicalProperties *p = NULL;
                    
                    if(info.modifiers.is(Modifiers::CENTROID))
                        p = fish->centroid(frame);
                    else if(info.modifiers.is(Modifiers::WEIGHTED_CENTROID))
                        p = fish->centroid_weighted(frame);
                    else if(info.modifiers.is(Modifiers::POSTURE_CENTROID))
                        p = fish->centroid_posture(frame);
                    else
                        p = fish->head(frame);
                    
                    if(p) {
                        average += p->pos(Units::PX_AND_SECONDS);
                        ++samples;
                    }
                }
            }
            
            if(samples)
                average /= samples;
            
            return average.length();
        });
        
        _cache_func["compactness"] = LIBGLFNC({
            Vec2 average(0);
            float samples = 0;
            
            std::vector<Vec2> positions;
            
            for(auto fish : Tracker::instance()->active_individuals(frame)) {
                if(fish->has(frame)) {
                    PhysicalProperties *p = NULL;
                    
                    if(info.modifiers.is(Modifiers::CENTROID))
                        p = fish->centroid(frame);
                    else if(info.modifiers.is(Modifiers::WEIGHTED_CENTROID))
                        p = fish->centroid_weighted(frame);
                    else if(info.modifiers.is(Modifiers::POSTURE_CENTROID))
                        p = fish->centroid_posture(frame);
                    else
                        p = fish->head(frame);
                    
                    if(p) {
                        positions.push_back(p->pos(Units::PX_AND_SECONDS));
                        average += positions.back();
                        ++samples;
                    }
                }
            }
            
            if(samples)
                average /= samples;
            
            float distances = 0;
            samples = 0;
            for(auto &pos : positions) {
                distances += (average - pos).length();
                ++samples;
            }
            
            return distances != 0 ? samples / distances : 0;
        });
        
        _cache_func["amplitude"] = LIBFNC({
            auto midline = fish->midline(frame);
            
            if (midline) {
                auto &pts = midline->segments();
                return (pts.back().pos - pts.front().pos).y;
            }
            
            return infinity<float>();
        });
        
        _cache_func["midline_length"] = LIBFNC({
            auto posture = fish->posture_stuff(frame);
            
            if (posture) {
                return posture->midline_length; //* FAST_SETTINGS(cm_per_pixel);
            }
            
            return infinity<float>();
        });
        
        SETTING(output_graphs) = SETTING(output_graphs).value<std::vector<std::pair<std::string, std::vector<std::string>>>>();
        
        
#pragma GCC diagnostic pop
    }
    
    void Library::InitVariables() {
        
    }
    
    void Library::frame_changed(long_t frame, LibraryCache::Ptr cache) {
        if(cache == nullptr)
            cache = _default_cache;
        
        std::lock_guard<std::recursive_mutex> lock(cache->_cache_mutex);
        for(auto &f : cache->_cache) {
            auto it = f.second.find(frame);
            if(it != f.second.end())
                f.second.erase(it);
        }
    }
    
    double Library::get(const std::string &name, LibInfo info, long_t frame) {
        auto _cache = info._cache;
        if(!_cache)
            _cache = _default_cache;
        
        std::lock_guard<std::recursive_mutex> lock(_cache->_cache_mutex);
        if(_cache_func.count(name) == 0)
            return infinity<float>();
        
        size_t cache_size = _cache->_cache.size();
        if (!info.rec_depth && cache_size > 100)
            _cache->clear();
        
        auto &cache = _cache->_cache[info.fish];
        cache_size = cache.size();
        if(!info.rec_depth && cache_size & 1 && cache.size() >= 50) {
            for(auto it=cache.begin(), ite=cache.end(); it!=ite;) {
                if(it->first < frame-25 || it->first > frame+25)
                    it = cache.erase(it);
                else
                    ++it;
            }
            
            if(cache.size() > 50)
                cache.clear();
        }
        
        auto &map = cache[frame][name];
        
        if (map.count(info.modifiers)) {
            return map.at(info.modifiers);
            
        } else {
            info.rec_depth++;
            const bool smooth = info.modifiers.is(SMOOTH);
            
            auto value = _cache_func.at(name)(info, frame, info.fish ? retrieve_props(name, info.fish, frame, info.modifiers) : NULL, smooth);
            map[info.modifiers] = value;
            return value;
        }
    };
    
    double Library::get_with_modifiers(const std::string &name, LibInfo info, long_t frame) {
        auto cache = info._cache;
        if(!cache)
            cache = _default_cache;
        
        std::lock_guard<std::recursive_mutex> lock(cache->_cache_mutex);
        if(_cache_func.count(name) == 0) {
            static std::string warning = "";
            if(warning != name) {
                warning = name;
                Warning("Cannot find output function '%S'.", &name);
            }
            return infinity<float>();
        }
        
        OptionsList<Modifiers> modifiers = info.modifiers;
        Calculation func;
        
        {
            std::lock_guard<std::mutex> guard(_output_variables_lock);
            if(_output_defaults.count(name)) {
                auto &o = _output_defaults.at(name);
                for(auto &e : o) {
                    if(!parse_modifiers(e, modifiers))
                        // function is something other than identity
                        func = parse_calculation(e);
                }
            }
        }
        
        return func.apply(Library::get(name, LibInfo(info.fish, modifiers, cache), frame));
    }
    
    void Library::add(const std::string& name, const FunctionType &func) {
        if (_cache_func.count(name)) {
            Warning("Overwriting '%S' with new function.", &name);
        }
        _cache_func[name] = func;
    }
    
    void Library::init_graph(Graph &graph, Individual *fish, LibraryCache::Ptr cache) {
        if(!cache)
            cache = _default_cache;
        
        std::lock_guard<std::mutex> guard(_output_variables_lock);
        auto &funcs = _options_map;
        auto annotations = SETTING(output_annotations).value<std::map<std::string, std::string>>();
        
        for (auto &f : funcs) {
            auto &fname = f.first;
            auto &instances = f.second;
            
            std::string units = "";
            if (annotations.count(fname)) {
                units = annotations.at(fname);
            }
            
            for (auto &e : instances) {
                LibInfo info(fish, e.first, cache);
                auto mod_name = fname;
                
                if (info.modifiers.is(SMOOTH))
                    mod_name += "#smooth";
                if(info.modifiers.is(CENTROID))
                    mod_name += "#centroid";
                else if(info.modifiers.is(POSTURE_CENTROID))
                    mod_name += "#pcentroid";
                else if(info.modifiers.is(WEIGHTED_CENTROID))
                    mod_name += "#wcentroid";
                
                auto func = Graph::Function(mod_name,
                    info.modifiers.is(POINTS) ? Graph::POINTS : Graph::DISCRETE,
                    [fname, mod_name, info, e](int x) {
                        return e.second.apply(Library::get(fname, info, x));
                        
                    }, gui::Color(), units);
                
                graph.add_function(func);
                
                if(info.modifiers.is(PLUSMINUS)) {
                    graph.add_function(Graph::Function(mod_name,
                       info.modifiers.is(POINTS) ? Graph::POINTS : Graph::DISCRETE,
                       [fname, mod_name, info, e](int x) {
                           return -e.second.apply(Library::get(fname, info, x));
                           
                       }, func._color, units));
                }
            }
        }
    }
    
    bool Library::has(const std::string &name) {
        return _cache_func.count(name) ? true : false;
    }
    
    const Calculation Library::parse_calculation(const std::string& calculation) {
        Calculation func;
        
        char operation = 0;
        std::stringstream number;
        
        for (uint32_t i=0; i<calculation.length(); i++) {
            char c = calculation.at(i);
            switch (c) {
                case '*': case '-': case '+': case '/':
                    assert(operation == 0);
                    operation = c;
                    break;
                    
                default:
                    number << c;
                    break;
            }
        }
        
        float nr = std::stof(number.str());
        
        switch (operation) {
            case '/':
                nr = 1.f/nr;
            case '*':
                func._operation = Calculation::Operation::MUL;
                func._factor = nr;
                break;
                
            case '-':
                nr = -nr;
            case '+':
                func._operation = Calculation::Operation::ADD;
                func._factor = nr;
                break;
                
            default:
                U_EXCEPTION("Unknown operator '%c'", operation);
                break;
        }
        
        return func;
    }
    
    bool Library::parse_modifiers(const std::string& e, OptionsList<Modifiers>& modifiers) {
        if (utils::lowercase(e) == "smooth") {
            modifiers.push(Modifiers::SMOOTH);
            
        } else if(utils::lowercase(e) == "raw") {
            modifiers.remove(Modifiers::SMOOTH);
        
        } else if(utils::lowercase(e) == "centroid") {
            modifiers.remove(Modifiers::POSTURE_CENTROID);
            modifiers.remove(Modifiers::WEIGHTED_CENTROID);
            
            modifiers.push(Modifiers::CENTROID);
            
        } else if(utils::lowercase(e) == "head") {
            modifiers.remove(Modifiers::CENTROID);
            modifiers.remove(Modifiers::POSTURE_CENTROID);
            modifiers.remove(Modifiers::WEIGHTED_CENTROID);
            
        } else if(utils::lowercase(e) == "pcentroid") {
            modifiers.remove(Modifiers::CENTROID);
            modifiers.remove(Modifiers::WEIGHTED_CENTROID);
            
            modifiers.push(Modifiers::POSTURE_CENTROID);
            
        } else if(utils::lowercase(e) == "wcentroid") {
            modifiers.remove(Modifiers::CENTROID);
            modifiers.remove(Modifiers::POSTURE_CENTROID);
            
            modifiers.push(Modifiers::WEIGHTED_CENTROID);
            
        } else if(utils::lowercase(e) == "points") {
            modifiers.push(Modifiers::POINTS);
        } else if(utils::lowercase(e) == "pm") {
            modifiers.push(Modifiers::PLUSMINUS);
        } else
            return false;
        
        return true;
    }
    
    void Library::remove_calculation_options() {
        auto &graphs = SETTING(output_graphs).value<graphs_type>();
        
        auto previous = _output_defaults;
        auto previous_graphs = graphs;
        
        default_options_type modified;
        OptionsList<Modifiers> modifiers; // temp object
        
        for (auto &p : previous) {
            auto &fname = p.first;
            auto &options = p.second;
            
            std::vector<std::string> tmp;
            for (auto &o : options) {
                if (parse_modifiers(o, modifiers))
                    tmp.push_back(o);
            }
            
            if (!tmp.empty())
                modified[fname] = tmp;
        }
        
        SETTING(output_default_options) = modified;
        
        graphs_type modified_graphs;
        
        for (auto &p : previous_graphs) {
            auto &fname = p.first;
            auto &options = p.second;
            
            std::vector<std::string> tmp;
            for (auto &o : options) {
                if (parse_modifiers(o, modifiers))
                    tmp.push_back(o);
            }
            
            //if (!tmp.empty())
            modified_graphs.push_back({fname, tmp});
        }
        
        SETTING(output_graphs) = modified_graphs;
    }
    
    float Library::tailbeats(long_t frame, Output::Library::LibInfo info) {
        auto fish = info.fish;
        
        float right = 0;
        float left = 0;
        float mx = -FLT_MAX;
        float mi = FLT_MAX;
        
        for (long_t offset=0; offset<100 && frame-offset >= fish->start_frame() && frame+offset <= fish->end_frame(); offset++)
        {
            auto f_l = frame-offset;
            auto f_r = frame+offset;
            
            if(!left) {
                auto v_l = Library::get(Functions::BINARY.name(), info, f_l);
                if(!cmn::isinf(v_l)) {
                    left = v_l;
                    //l_idx = f_l;
                } else {
                    auto y = Library::get("fixed_midline", info, f_l);
                    if(cmn::isinf(y))
                        return 0;
                    
                    if (!cmn::isinf(y) && cmn::abs(y) > cmn::abs(mx))
                        mx = y;
                    if (y < mi)
                        mi = y;
                }
            }
            
            if(!right && offset) {
                auto v_r = Library::get(Functions::BINARY.name(), info, f_r);
                if(!cmn::isinf(v_r)) {
                    right = v_r;
                    //r_idx = f_r;
                } else {
                    auto y = Library::get("fixed_midline", info, f_r);
                    if(cmn::isinf(y))
                        return 0;
                    
                    if (!cmn::isinf(y) && cmn::abs(y) > cmn::abs(mx))
                        mx = y;
                    if (y < mi)
                        mi = y;
                }
            }
            
            if(left && right)
                break;
        }
        
        auto y = Library::get("MIDLINE_OFFSET", info, frame);
        if (right
            && left
            && cmn::abs(y) >= SETTING(limit).value<float>())
        {
            return mx;
        }
        
        return 0;
    }
    
    bool save_focussed_on(const file::Path& file, const Individual* fish) {
        using namespace file;
        
        std::vector<std::string> header = {"frame"};
        std::vector<std::string> nheader = {"x", "y", "angle", "lv", "la"};
        
        header.insert(header.end(), nheader.begin(), nheader.end());
        std::vector<Individual*> neighbors;
        
        Tracker::LockGuard guard("save_focussed_on");
        
        for (auto && [id, neighbor] : Tracker::individuals()) {
            if(neighbor != fish) {
                neighbors.push_back(neighbor);
                
                header.insert(header.end(), nheader.begin(), nheader.end());
                for(size_t i=header.size()-nheader.size(); i<header.size(); i++)
                    header[i] += std::to_string(neighbors.size());
            }
        }
        
        Table table(header);
        Row row;
        
        for (long_t frame=Tracker::start_frame(); frame<=Tracker::end_frame(); frame++) {
            row.clear();
            
            cv::Mat data = cv::Mat::zeros(1, 100, CV_32FC1);
            cv::Mat input, fft, inverse;
            cv::merge(std::vector<cv::Mat>{data, cv::Mat::zeros(data.size(), CV_32FC1)}, input);
            
            cv::dft(input, fft, cv::DFT_COMPLEX_OUTPUT);
            cv::idft(fft, inverse, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT);
            
            auto prop = fish->centroid_posture(frame);
            if(prop) {
                float x = prop->pos().x;
                float y = prop->pos().y;
                float angle = prop->angle();
                float lv = prop->v().length();
                float la = prop->a().length();
                
                row.add(frame);
                row.add(x).add(y).add(angle).add(lv).add(la);
                
                for(size_t i=0; i<neighbors.size(); i++) {
                    auto n = neighbors[i];
                    auto nprop = n->centroid_posture(frame);
                    
                    if(nprop) {
                        float b = nprop->angle();
                        row .add(nprop->pos().x - x)
                            .add(nprop->pos().y - y)
                            .add(atan2(sin(b-angle), cos(b-angle)))
                            .add(nprop->v().length() - lv)
                            .add(nprop->a().length() - la);
                        
                    } else {
                        row.repeat("", nheader.size());
                    }
                }
                
                table.add(row);
            }
        }
        
        CSVExport e(table);
        return e.save(file);
    }
}